﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Channels;
using System.ServiceModel.Security.Tokens;
using Sinacor.Infra.Common.Security.Authentication.Credentials;
using System.ServiceModel;
using Sinacor.Infra.Common.Security.Authentication.ClientCredentials;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel.Description;
using System.Xml;
using System.IdentityModel.Tokens;
using PSE.Framework.Configuration;
using PSE.Framework.Common.Security.Services.Configuration;

namespace Sinacor.Infra.Common.Security.Services
{
    /// <summary>
    /// Classe para auxiliar a criação dos Bindings de serviço.
    /// </summary>
    public class BindingHelper
    {
        //constantes para identificacao de protocolo
        private const string HttpProtocol = "Http";
        private const string TcpProtocol = "Tcp";
        private const string NamedPipeProtocol = "NamedPipe";
        private const string SERVICES_CONFIGURATION_SECTION_NAME = "servicesConfiguration";
        private const string CERTIFICATE_CONFIGURATION_SECTION_NAME = "certificateConfiguration";

        /// <summary>
        /// Recuperar o protocolo configurado para o endpoint
        /// </summary>
        /// <param name="serviceEndpoint"></param>
        /// <returns></returns>
        public static string ResolveProtocol(ServiceEndpoint serviceEndpoint)
        {
            return ResolveProtocol(serviceEndpoint.Address);
        }

        /// <summary>
        /// Recuperar o protocolo configurado para o endpoint
        /// </summary>
        /// <param name="endpoint"></param>
        /// <returns></returns>
        public static string ResolveProtocol(EndpointAddress endpoint)
        {
            if (endpoint != null && endpoint.Uri != null)
            {
                string schema = endpoint.Uri.Scheme;

                switch (schema.ToUpper())
                {
                    case "NET.TCP":
                        return TcpProtocol;
                    case "NET.PIPE":
                        return NamedPipeProtocol;
                    case "HTTP":
                        return HttpProtocol;
                }
            }

            return string.Empty;
        }

        public static Binding CreateBinding(string protocol)
        {
            return CreateBinding(protocol, null);
        }

        /// <summary>
        /// Selector para criação do Binding.
        /// </summary>
        /// <param name="protocol">Nome do protocolo</param>
        /// <returns>Binding com transporte sob o módulo especificado</returns>
        public static Binding CreateBinding(string protocol, TransferMode? transferMode)
        {
            switch (protocol)
            {
                case HttpProtocol: return CreateHttpTransportSinacorBinding();
                case TcpProtocol: return CreateTcpTransportSinacorBinding(transferMode);
                case NamedPipeProtocol: return CreateNamedPipeTransportSinacorBinding();
                default:
                    throw new ArgumentException("Invalid protocol {" + protocol + "}");
            }
        }

        /// <summary>
        /// Retorna o nome do contrato, o modulo e o submodulo.
        /// </summary>
        /// <param name="module">Modulo do Servico</param>
        /// <param name="submodule">SubModulo do Servico</param>
        /// <returns>Nome do contrato</returns>
        public static string RetrieveContractName<Tproxy>(ref string module, ref string submodule)
        {
            //Pega o namespace do serviço
            string nmspace = typeof(Tproxy).Namespace;
            //Pega o nome do serviço
            string service = typeof(Tproxy).Name;

            string[] split = nmspace.Split('.');
            //Pega o nome do Modulo
            module = split[split.Length - 3];
            //Pega o nome do SubModulo
            submodule = split[split.Length - 2];
            //pega o namespace do contrato do serviço
            string serviceNamespace = split[split.Length - 1];

            //Monta a chave do arquivo de configuração
            string contractName = module + "." + submodule + "." + serviceNamespace + "." + service;

            //Retorna o endpoint
            return contractName;
        }

        public static EndpointConfigurationElement RetrieveEndpointConfiguration(string contractName)
        {
            EndpointConfigurationSection endpointConfiguration = ApplicationConfiguration.GetCurrent().GetSection<EndpointConfigurationSection>("endpointConfiguration");
            string endpointConfigPath = System.IO.Path.Combine(ApplicationConfiguration.GetApplicationRootDirectory(), endpointConfiguration.SectionInformation.ConfigSource);

            foreach (EndpointConfigurationElement element in endpointConfiguration.Endpoints)
            {
                if (element.Contract == contractName)
                    return element;
            }

            throw new Exception(string.Format("The contract '{0}' does not exist in file '{1}'.", contractName, endpointConfigPath));
        }

        #region Service Transport

        /// <summary>
        /// Cria um Binding sob o protocolo Http para o Sinacor
        /// </summary>
        /// <returns>CustomBinding</returns>
        public static Binding CreateHttpTransportSinacorBinding()
        {
            //TODO: Incluir o Issuer Http
            HttpTransportBindingElement httpTransport = new HttpTransportBindingElement();

            httpTransport.MaxBufferPoolSize = 122880;
            httpTransport.MaxBufferSize = Int32.MaxValue;
            httpTransport.MaxReceivedMessageSize = Int32.MaxValue;

            SymmetricSecurityBindingElement messageSecurity = new SymmetricSecurityBindingElement();
            messageSecurity.EndpointSupportingTokenParameters.SignedEncrypted.Add(new SinacorTokenParameters());
            X509SecurityTokenParameters x509ProtectionParameters = new X509SecurityTokenParameters();
            x509ProtectionParameters.InclusionMode = SecurityTokenInclusionMode.Never;
            messageSecurity.ProtectionTokenParameters = x509ProtectionParameters;

            return new CustomBinding(messageSecurity, httpTransport);
        }

        /// <summary>
        /// Cria um Binding sob o protocolo TCP para o Sinacor
        /// </summary>
        /// <returns>CustomBinding</returns>
        public static Binding CreateTcpTransportSinacorBinding(TransferMode? transferMode)
        {
            CustomBinding customBinding = null;

            //Definição do protocolo de transporte do Binding
            TcpTransportBindingElement tcpTransport = new TcpTransportBindingElement();
            tcpTransport.MaxBufferPoolSize = 122880;
            tcpTransport.MaxBufferSize = Int32.MaxValue;
            tcpTransport.MaxReceivedMessageSize = Int32.MaxValue;
            tcpTransport.ConnectionBufferSize = 122880;

            //Definição da Formatação da mensagem
            BinaryMessageEncodingBindingElement messageEncoding = new BinaryMessageEncodingBindingElement();

            //Criação da segurança em binding no formato Stremead
            WindowsStreamSecurityBindingElement windowsStreamSecurityBindingElement = new WindowsStreamSecurityBindingElement();

            SecurityBindingElement messageSecurity = null;

            //Recuperação do Binding e Endpoint do STS
            Binding issuerBinding = null;
            EndpointAddress issuerEndPoint = null;
            GetIssuerConfiguration(out issuerEndPoint, out issuerBinding);

            //Criação do token a ser utilizado no binding de segurança
            IssuedSecurityTokenParameters issuedSecurityTokenParameters = new IssuedSecurityTokenParameters(Constants.SamlTokenType, issuerEndPoint, issuerBinding);
            SecureConversationSecurityTokenParameters secureConversationSecurityTokenParameters = null;

            if (transferMode == null || transferMode == TransferMode.Buffered)
            {
                //Criação da segurança na mensagem
                secureConversationSecurityTokenParameters = new SecureConversationSecurityTokenParameters(SecurityBindingElement.CreateIssuedTokenBindingElement(issuedSecurityTokenParameters));

                messageSecurity = new SymmetricSecurityBindingElement();
                ((SymmetricSecurityBindingElement)messageSecurity).ProtectionTokenParameters = secureConversationSecurityTokenParameters;

                customBinding = new CustomBinding(messageSecurity, messageEncoding, tcpTransport);
            }
            else
            {
                //Criação da segurança no transporte
                secureConversationSecurityTokenParameters = new SecureConversationSecurityTokenParameters(SecurityBindingElement.CreateIssuedTokenOverTransportBindingElement(issuedSecurityTokenParameters));

                messageSecurity = new TransportSecurityBindingElement();
                messageSecurity.EndpointSupportingTokenParameters.Endorsing.Clear();
                messageSecurity.EndpointSupportingTokenParameters.Endorsing.Add(secureConversationSecurityTokenParameters);

                secureConversationSecurityTokenParameters.BootstrapSecurityBindingElement.EndpointSupportingTokenParameters.Endorsing.Clear();
                secureConversationSecurityTokenParameters.BootstrapSecurityBindingElement.EndpointSupportingTokenParameters.Endorsing.Add(issuedSecurityTokenParameters);

                customBinding = new CustomBinding(messageSecurity, messageEncoding, windowsStreamSecurityBindingElement, tcpTransport);
            }

            return customBinding;
        }

        /// <summary>
        /// Cria um Binding sob o protocolo TCP para o Sinacor
        /// </summary>
        /// <returns>CustomBinding</returns>
        public static Binding CreateNamedPipeTransportSinacorBinding()
        {
            //Definição do protocolo de transporte do Binding
            NamedPipeTransportBindingElement namePipeTransport = new NamedPipeTransportBindingElement();

            namePipeTransport.MaxBufferPoolSize = 122880;
            namePipeTransport.MaxBufferSize = Int32.MaxValue;
            namePipeTransport.MaxReceivedMessageSize = Int32.MaxValue;
            namePipeTransport.ConnectionBufferSize = 122880;

            //Definição da segurança da mensagem.
            SymmetricSecurityBindingElement messageSecurity = new SymmetricSecurityBindingElement();

            Binding issuerBinding = null;
            EndpointAddress issuerEndPoint = null;

            GetIssuerConfiguration(out issuerEndPoint, out issuerBinding);

            //Criação do Issued Token
            messageSecurity.ProtectionTokenParameters =
                new SecureConversationSecurityTokenParameters(SecurityBindingElement.CreateIssuedTokenBindingElement(
                            new IssuedSecurityTokenParameters(Constants.SamlTokenType,
                            issuerEndPoint, issuerBinding)));

            //Definição da Formatação da mensagem
            BinaryMessageEncodingBindingElement messageEncoding = new BinaryMessageEncodingBindingElement();

            return new CustomBinding(messageSecurity, messageEncoding, namePipeTransport);
        }

        /// <summary>
        /// Adicionar o Binding do STS dentro do Binding de serviço
        /// </summary>
        public static void AddIssuerBinding(Binding bindingRef)
        {
            CustomBinding cb = bindingRef as CustomBinding;
            SecurityBindingElement messageSecurity = cb.Elements.Find<SecurityBindingElement>();

            if (messageSecurity != null)
            {
                Binding issuerBinding = null;
                System.ServiceModel.EndpointAddress issuerEndPoint = null;

                GetIssuerConfiguration(out issuerEndPoint, out issuerBinding);

                if (messageSecurity is SymmetricSecurityBindingElement)
                {
                    IssuedSecurityTokenParameters parameters = new IssuedSecurityTokenParameters(Constants.SamlTokenType, issuerEndPoint, issuerBinding);
                    ((SymmetricSecurityBindingElement)messageSecurity).ProtectionTokenParameters = new SecureConversationSecurityTokenParameters(SecurityBindingElement.CreateIssuedTokenBindingElement(parameters));
                }
                else if (messageSecurity is TransportSecurityBindingElement &&
                    messageSecurity.EndpointSupportingTokenParameters.Endorsing.Count > 0 &&
                    messageSecurity.EndpointSupportingTokenParameters.Endorsing[0] is SecureConversationSecurityTokenParameters)
                {
                    SecureConversationSecurityTokenParameters p = messageSecurity.EndpointSupportingTokenParameters.Endorsing[0] as SecureConversationSecurityTokenParameters;

                    if (p.BootstrapSecurityBindingElement.EndpointSupportingTokenParameters.Endorsing.Count > 0)
                    {
                        IssuedSecurityTokenParameters parameters = p.BootstrapSecurityBindingElement.EndpointSupportingTokenParameters.Endorsing[0] as IssuedSecurityTokenParameters;

                        if (parameters != null)
                        {
                            parameters.IssuerAddress = issuerEndPoint;
                            parameters.IssuerBinding = issuerBinding;
                        }
                    }
                }
            }
        }

        #endregion

        #region Issuer Transport

        /// <summary>
        /// Cria um Binding para o Issuer (STS) Sinacor sob o protocolo TCP
        /// </summary>
        /// <returns>CustomBinding</returns>
        private static Binding CreateTcpTransportIssuerBinding()
        {
            TcpTransportBindingElement tcpTransport = new TcpTransportBindingElement();

            SymmetricSecurityBindingElement messageSecurity = new SymmetricSecurityBindingElement();

            messageSecurity.EndpointSupportingTokenParameters.SignedEncrypted.Add(new SinacorTokenParameters());
            X509SecurityTokenParameters x509ProtectionParameters = new X509SecurityTokenParameters();
            x509ProtectionParameters.InclusionMode = SecurityTokenInclusionMode.Never;
            messageSecurity.ProtectionTokenParameters = x509ProtectionParameters;

            SecurityBindingElement security = SecurityBindingElement.CreateSecureConversationBindingElement(messageSecurity);

            //Definição da Formatação da mensagem
            BinaryMessageEncodingBindingElement messageEncoding = new BinaryMessageEncodingBindingElement();

            return new CustomBinding(security, messageEncoding, tcpTransport);
        }

        /// <summary>
        /// Cria um Binding para o Issuer (STS) Sinacor sob o protocolo Http
        /// </summary>
        /// <returns>CustomBinding</returns>
        public static Binding CreateHttpTransportIssuerBinding()
        {
            HttpTransportBindingElement httpTransport = new HttpTransportBindingElement();

            httpTransport.MaxBufferPoolSize = 122880;
            httpTransport.MaxBufferSize = Int32.MaxValue;
            httpTransport.MaxReceivedMessageSize = Int32.MaxValue;

            SymmetricSecurityBindingElement messageSecurity = new SymmetricSecurityBindingElement();
            messageSecurity.EndpointSupportingTokenParameters.SignedEncrypted.Add(new SinacorTokenParameters());
            X509SecurityTokenParameters x509ProtectionParameters = new X509SecurityTokenParameters();
            x509ProtectionParameters.InclusionMode = SecurityTokenInclusionMode.Never;
            messageSecurity.ProtectionTokenParameters = x509ProtectionParameters;
            return new CustomBinding(messageSecurity, httpTransport);
        }

        /// <summary>
        /// Retorna o endereço do Emissor de Token configurado.
        /// </summary>
        /// <returns></returns>
        private static EndpointAddress RetrieveIssuerEndpoint()
        {
            ApplicationConfiguration config = ApplicationConfiguration.GetCurrent();

            string endpointAddress = config.GetSection<ServicesConfigurationSection>(SERVICES_CONFIGURATION_SECTION_NAME).IssuerAddress;
            if (String.IsNullOrEmpty(endpointAddress))
                throw new ArgumentNullException("The issuer address configuration is not set");

            string certificate = config.GetSection<CertificateConfigurationSection>(CERTIFICATE_CONFIGURATION_SECTION_NAME).STSAuthorityCertificate.Name;

            return new EndpointAddress(new Uri(endpointAddress), new DnsEndpointIdentity(certificate));
        }

        /// <summary>
        /// Monta o endpoint e o Binding Configurado para o Issuer.
        /// </summary>
        /// <param name="endpoint"></param>
        /// <param name="binding"></param>
        private static void GetIssuerConfiguration(out EndpointAddress endpoint, out Binding binding)
        {
            endpoint = RetrieveIssuerEndpoint();

            if (endpoint.Uri.AbsoluteUri.ToUpper().StartsWith("HTTP"))
                binding = CreateHttpTransportIssuerBinding();
            else
                binding = CreateTcpTransportIssuerBinding();
        }

        #endregion
    }
}
